'''
An RPG Modification for Source games using Source Python.

    Copyright (C) 2012 Steven Hartin

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Created on 4 Dec 2012

@author: Steven Hartin
@file: ../sourcerpg/database/parser.py
'''

__all__ = ["parser_commands", "TableStructure", "TableRowStructure",
           "DatabaseStructure", "row_types", "BaseParser"]

import hashlib
from xml.etree.ElementTree import ElementTree
from operator import attrgetter
from collections import abc, deque
from configobj import ConfigObj

from path import path

from sourcerpg.utils.enum import Enum
from sourcerpg.utils.baseclass import BaseClass
from sourcerpg.logging import logger


class DatabaseCommandQueue(deque):
    """A queue to store a command queue to create the structure of databases"""
    def append(self, other):
        """Overriden from base, ensure type coherency

        @param DatabaseCommand other Object to store inside the queue

        """
        if not isinstance(other, DatabaseCommand):
            raise TypeError("DatabaseComandQueue can only store objects of"
                "type 'DatabaseCommand', received type '%s'" % (
                    type(other).__name__))
        super(DatabaseCommandQueue, self).append(other)


class DatabaseCommand(BaseClass):
    """Stores a list of arguments and a command to execute on a database API"""
    def __init__(self, command, **kwargs):
        if command not in parser_commands:
            raise TypeError("%s is not a valid command" % command)
        self.command = command
        self.args = kwargs

    def get_command(self):
        """Wrap the get_<attr> functionality for the command too.

        @return ParserCommand The command this database command represents.

        """
        return self.command

    def __getattr__(self, attr):
        """Allows us to use get_<attr>() calls to acquire access to attributes

        E.g. If we have a 'name' attribute for this command, we could use
        get_name() to obtain the name, or quite simply .name to access the
        attribute directly. If we use the get_<attr> accessor, we return a
        lambda to allow us to have a callbale function.

        @param str attr The name of the attribute which was not found
        @return mixed The value associated with that key (if it exists)

        """
        if attr.startswith("get_"):
            if attr.lstrip("get_") in self.args:
                return lambda: self.args[attr.lstrip("get_")]
        else:
            if attr in self.args:
                return self.args[attr]
        raise AttributeError("Attribute '%s' does not exist" % attr)


class DatabaseStructure(list):
    """The structure of the database.

    This class should contain the structure of the database. The structure can
    be defined as a container of tables / data.

    """
    def compare(self, other):
        """Returns a command queue of the differences between another structure

        @param DatabaseStructure other
        @return DatabaseCommandQueue A queue of database commands which differ
            between the two structure

        """
        difference = DatabaseCommandQueue()

        if not isinstance(other, DatabaseStructure):
            raise TypeError("Expected type 'DatabaseStructure', received %s" %
                type(other).__name__)
        # Because sets reorder using a binary tree, we've got to create two
        # sets. One for the <name, object> pair to allow us to retain the
        # instance at the right index, and the other to obtain the differences.
        selfset = set(map(lambda x: (attrgetter("name")(x), x), self))
        otherset = set(map(lambda x: (attrgetter("name")(x), x), other))
        # Modify to just obtain the name
        selfnameset = set(map(lambda x: x[0], selfset))
        othernameset = set(map(lambda x: x[0], otherset))
        for index, diff in enumerate(selfnameset.difference(othernameset)):
            # For all the tables which need adding
            table = selfset[index][1]
            command = DatabaseCommand(
                parser_commands.CREATE_TABLE,
                name=diff,
                rows=table
                )
            difference.append(command)
        for diff in othernameset.difference(selfnameset):
            # For all the tables which need removing
            command = DatabaseCommand(
                parser_commands.DELETE_TABLE,
                name=diff)
            difference.append(command)
        for index, diff in enumerate(selfset.intersection(otherset)):
            # For all the tables which need checking for consistency
            selfset[index].compare(otherset[index], difference)
        return difference

    def get_command_queue(self):
        """Obtains a queue of commands required in order to create the database

        @return DatabaseCommandQueue Queue of commands

        """
        command_queue = DatabaseCommandQueue()
        for table in self:
            command = DatabaseCommand(
                parser_commands.CREATE_TABLE,
                name=table.name,
                rows=table)
            command_queue.append(command)
        return command_queue

    def append(self, other):
        """Overriden from base class of list, ensure type coherency.

        @param TableStructure other An item to append to the list

        """
        if not isinstance(other, TableStructure):
            raise TypeError("Expected type 'TableStructure', received '%s'" %
                type(other).__name__)
        super(__class__, self).append(other)


class TableStructure(list):
    """Class to hold a structure of a table for a database.

    This class should contain the structure of t able. The structure can be
    defined as a container of rows.

    """
    def __init__(self, name):
        self.name = name

    def append(self, other):
        """Overriden from base class of list, ensure type coherency.

        @param TableRowStructure other An item to append to the list

        """
        if not isinstance(other, TableRowStructure):
            raise TypeError("Expected type 'TableRowStructure', received %s" %
                type(other).__name__)
        super(__class__, self).append(other)

    def compare(self, other, diff_command_queue):
        """Compares all the rows in this table to ensure consistency

        @param other TableStructure The other object to compare against
        @param DatabaseCommandQueue The command queue to append the difference
            of commands to.

        """
        if not isinstance(other, TableStructure):
            raise TypeError("Expected type 'TableStructure', received %s" %
                type(other).__name__)
        # Because sets reorder using a binary tree, we've got to create two
        # sets. One for the <name, object> pair to allow us to retain the
        # instance at the right index, and the other to obtain the differences.
        selfset = set(map(lambda x: (attrgetter("name")(x), x), self))
        otherset = set(map(lambda x: (attrgetter("name")(x), x), other))
        # Modify to just obtain the name
        selfnameset = set(map(lambda x: x[0], selfset))
        othernameset = set(map(lambda x: x[0], otherset))

        for index, diff in enumerate(selfnameset.difference(othernameset)):
            # For all the tables which need adding
            row = selfset[index][1]
            command = DatabaseCommand(
                parser_commands.ALTER_TABLE,
                subcommand=parser_commands.CREATE_ROW,
                row=row
                )
            diff_command_queue.append(command)
        for diff in othernameset.difference(selfnameset):
            # For all the tables which need removing
            command = DatabaseCommand(
                parser_commands.ALTER_TABLE,
                subcommand=parser_commands.DELETE_ROW,
                name=diff)
            diff_command_queue.append(command)
        for index, diff in enumerate(selfset.intersection(otherset)):
            # For all the tables which need checking for consistency
            self_row = selfset[index][1]
            other_row = otherset[index][1]
            if (self_row.type_ != other_row.type_ or
                self_row.default != other_row.default or
                self_row.maxlength != other_row.maxlength or
                self_row.default is not other_row.default or
                self_row.not_null is not other_row.not_null or
                self_row.primary_key is not other_row.primary_key):
                command = DatabaseCommand(
                    parser_commands.ALTER_TABLE,
                    subcommand=parser_commands.ALTER_ROW,
                    row=self_row)
                diff_command_queue.append(command)


class TableRowStructure(BaseClass):
    """Stores a representation of a row structure in a database"""
    def __init__(self, name, type_, default=None, maxlength=None,
        primary_key=False, unique=False, not_null=False, auto_increment=False):
        self.name = name
        self.type_ = type_
        self.default = default
        self.maxlength = maxlength
        self.primary_key = primary_key
        self.unique = unique
        self.not_null = not_null
        self.auto_increment = auto_increment


class ParserException(SyntaxError):
    pass


class BaseParser(BaseClass):
    """Parse an XML document and fill out a database structure"""

    # This class is an abstract base class, fill out the meta info
    __metaclass__ = abc.ABCMeta

    def __init__(self, src_file, meta_file, database):
        """Constructor, create the database structure and parse the input

        @param str src_file A path to the XML source file
        @param str meta_file The file path of the meta data
        @param BaseDatabase database The database to parse the data to

        """
        self.structure = DatabaseStructure()
        meta = ConfigObj(meta_file)
        with open(src_file, "r") as src_handle:
            src = src_handle.read()
            md5 = hashlib.md5(src.encode()).hexdigest()
            if "md5" not in meta:
                logger.log("Installing database")
                self.__parse(src_file, database)
                meta["md5"] = md5
                meta["src"] = src
                meta.write()
            elif meta["md5"] != md5:
                logger.log("Modifying database")
                # Compare the two command structures
                # Parse the source into a table structure
                src_structure = self.__parse(src)
                # Parse the current database into a table structure
                current_structure = self.__parse(meta["src"])
                # Obtain the difference between the two
                diff_command_queue = src_structure.compare(current_structure)
                # Parse the commands to alter the table to the new src
                self.parse_command_queue(diff_command_queue, database)
                # Update the meta file
                meta["md5"] = md5
                meta["src"] = src
                meta.write()

    def __parse(self, src, database=None):
        """Parse the file at the given path

        Opens the file, pass it to the element tree parser and parse the XMl
        file. This XML file should describe the structure of the database.

        @param str src Either a path to the source file or the XML source
        @param BaseDatabase database A database instance

        """
        xml_tree = ElementTree()
        xml_tree.parse(src)
        root = xml_tree.getroot()

        database_structure = DatabaseStructure()
        for table in root.iter("table"):

            if "name" not in table.attrib:
                raise ParserException("Table elements must have a name "
                    "attribute")
            table_structure = TableStructure(table.attrib["name"])
            self.structure.append(table_structure)

            for row in table.iter("row"):
                if "name" not in row.attrib:
                    raise ParserException("row elements must have a name "
                        "attribute")
                if "type" not in row.attrib:
                    raise ParserException("row elements must have a type "
                        "attribute")
                if row.attrib["type"] not in row_types:
                    raise ParserException("row '%s' is not a valid type" %
                        row.attrib["type"])
                name = row.attrib["name"]
                type_ = row_types[row.attrib["type"]]
                default = row.attrib.get("default", None)
                maxlength = row.attrib.get("maxlength", None)
                primary_key = "primary_key" in row.attrib
                unique = "unique" in row.attrib
                not_null = "not_null" in row.attrib
                auto_increment = "auto_increment" in row.attrib
                row_structure = TableRowStructure(
                    name, type_, default, maxlength, primary_key,
                    unique, not_null, auto_increment)

                table_structure.append(row_structure)
            database_structure.append(table_structure)
        if database is not None:
            self.parse_command_queue(database_structure.get_command_queue(),
                database)
        else:
            return database_structure

    @abc.abstractmethod
    def parse_command_queue(self, database):
        raise NotImplementedError("parse_command_queue not implemented in "
            "the parser API")


class ParserCommands(Enum):
    """An enumeration of potential parser commands to execute on a database"""
    pass


class RowTypes(Enum):
    """An enumeration of potential row types a column could be in a table"""
    pass


parser_commands = ParserCommands(
    "UNKNOWN_COMMAND",
    "CREATE_TABLE",
    "DELETE_TABLE",
    "ALTER_TABLE",
    "CREATE_ROW",
    "DELETE_ROW",
    "MODIFY_ROW")

row_types = RowTypes(
    "UNKNOWN",
    "BOOLEAN",
    "CHAR",
    "INTEGER",
    "STRING",
    "BLOB",
    "REAL",
    "DATE",
    "DATETIME"
)
